/* eslint-disable no-lone-blocks */
/* eslint-disable multiline-comment-style */
import type fs from 'fs-extra'
import type { SandstoneCommands } from 'sandstone/commands/commands'
import type { Node, SandstoneCore } from 'sandstone/core'
import type { ResourcePath, SandstonePack } from 'sandstone/pack'
import type { PackType } from 'sandstone/pack/packType'
import type { BASIC_CONFLICT_STRATEGIES, LiteralUnion, MakeInstanceCallable } from 'sandstone/utils'

export type ResourceClassArguments<ConflictType extends 'default' | 'list' | 'function'> = {
  /**
   * Whether the associated Node should be added to Sandstone Core.
   */
  addToSandstoneCore: boolean

  /**
   * Whether the resource has been created explicitely by a user, or automatically generated by Sandstone.
   */
  creator: 'user' | 'sandstone'

  /**
   * What to do if another resource of the same type has the same name.
   *
   * - `throw`: Throw an error.
   * - `replace`: Replace silently the old resource with the new one.
   * - `ignore`: Keep silently the old resource, discarding the new one.
   * - `append`: Append the new resource entries to the old one.
   * - `prepend`: Prepend the new resource entries to the old one.
   * - `rename`: Rename the new file to an iterating number (ie. func1, func2, func3)
   */
  // eslint-disable-next-line max-len
  onConflict?: ConflictType extends 'default' ? BASIC_CONFLICT_STRATEGIES : BASIC_CONFLICT_STRATEGIES | 'append' | 'prepend',

  /**
   * Overrides the default pack for the resource.
   */
  packType?: PackType
}

export type ResourceNode<T = ResourceClass<any>> = Node & {
  resource: T
}

export type ResourceNodeConstructor<N extends Node> = new (sandstoneCore: SandstoneCore, resource: any) => N

export abstract class ResourceClass<N extends ResourceNode = ResourceNode<any>> {
  protected node: N

  packType

  fileExtension

  fileEncoding

  protected creator: NonNullable<ResourceClassArguments<any>['creator']>

  protected commands: SandstoneCommands<false>

  protected pack: SandstonePack

  path

  addToSandstoneCore: boolean

  onConflict: LiteralUnion<BASIC_CONFLICT_STRATEGIES>

  renameIndex = 2

  // eslint-disable-next-line max-len
  constructor(protected core: SandstoneCore, file: { packType: PackType, extension?: string, encoding?: fs.EncodingOption | false }, NodeType: ResourceNodeConstructor<N>, path: ResourcePath, args: ResourceClassArguments<any>) {
    this.node = new NodeType(core, this)

    this.packType = args.packType || file.packType

    this.fileExtension = file.extension
    this.fileEncoding = file.encoding === undefined ? 'utf8' : file.encoding

    this.pack = core.pack
    this.commands = core.pack.commands

    this.path = path

    this.addToSandstoneCore = args.addToSandstoneCore

    this.creator = args.creator ?? 'sandstone'

    const scopedStrategy = this.node.resource.path[1] ? process.env[`${this.node.resource.path[1].toUpperCase()}_CONFLICT_STRATEGY`] : false

    this.onConflict = args.onConflict || scopedStrategy || process.env.DEFAULT_CONFLICT_STRATEGY || 'throw'
  }

  protected handleConflicts() {
    const resourceType = this.node.resource.path[1] || 'resources'

    const conflict = [...this.core.resourceNodes].find((node) => {
      if (node.resource.packType.constructor.name !== this.node.resource.packType.constructor.name) {
        return false
      }
      return node.resource.packType.constructor === this.node.resource.packType.constructor && node.resource.path.join('') === this.node.resource.path.join('')
    })

    let add = false

    if (conflict) {
      const oldResource = conflict.resource
      const newResource = this.node.resource

      switch (this.onConflict) {
        case 'throw': {
          // eslint-disable-next-line max-len
          throw new Error(`Created a ${resourceType.substring(0, resourceType.length - 1)} with the duplicate name ${newResource.name}, and onConflict was set to "throw".`)
        }
        case 'replace': {
          this.core.resourceNodes.forEach((node) => {
            if (node.resource.path.join('') === oldResource.path.join('')) {
              this.core.resourceNodes.delete(node)
            }
          })
          add = true
        } break
        case 'warn': {
          console.warn([
            'Warning:',
            `Tried to create a ${resourceType.substring(0, resourceType.length - 1)} named "${newResource.name}", but found an already existing one.`,
            "The new one has replaced the old one. To remove this warning, please change the options of the resource to { onConflict: '/* other option */' }.",
          ].join('\n'))
          this.core.resourceNodes.forEach((node) => {
            if (node.resource.path.join('') === oldResource.path.join('')) {
              this.core.resourceNodes.delete(node)
            }
          })
          add = true
        } break
        case 'rename': {
          // eslint-disable-next-line no-plusplus
          this.path[this.path.length - 1] += `${oldResource.renameIndex++}`

          add = true
        } break
        case 'prepend': {
          (oldResource as unknown as ListResource).unshift(newResource)
        } break
        case 'append': {
          (oldResource as unknown as ListResource).push(newResource)
        } break
        default: break
      }
    } else {
      add = true
    }
    if (this.addToSandstoneCore && add) {
      this.core.resourceNodes.add(this.node)
    }
  }

  protected getNode = () => this.node

  get name(): string {
    return `${this.path[0]}:${this.path.slice(2).join('/')}`
  }

  protected generate = () => {}

  toString(): string {
    return this.name
  }
}

export abstract class CallableResourceClass<N extends ResourceNode = ResourceNode<any>> extends ResourceClass<N> {
  // This is the resource, but as a callable
  private _that: MakeInstanceCallable<this> | undefined

  protected get asCallable() {
    if (!this._that) {
      throw new Error('Class has not been made callable.')
    }
    return this._that
  }

  private makeCallable(mcFunctionClass: this & this['__call__']) {
    this._that = mcFunctionClass
    this.node.resource = mcFunctionClass
  }

  abstract __call__: (...args: any) => any
}

export abstract class ListResource {
  public push(...args: any[]) {}

  public unshift(...args: any[]) {}
}
